/**
 * Copyright (C) 2014
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package psn.shan.xml.enhancedvalidator.rules;

import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Vector;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Namespace;
import org.jdom2.filter.ElementFilter;

import psn.shan.xml.enhancedvalidator.rules.comparator.AbstractComparator;
import psn.shan.xml.enhancedvalidator.rules.comparator.ComparatorFactory;
import psn.shan.xml.enhancedvalidator.rules.comparator.ComparisonMethodType;
import psn.shan.xml.enhancedvalidator.rules.comparator.OperatorType;
import psn.shan.xml.enhancedvalidator.util.XmlUtils;

/**
 * RuleSetManager <br /> 
 * 
 * Manager to load and process rules
 * 
 * @author Shan
 * 
 */
@Named
@Slf4j
public class RuleSetManager<T extends AbstractRuleSet> {
	
	// Factory to get the appropriate Rules based on RuleType 
	@Inject
	private RuleFactory mRuleFactory;
	
	// The RuleSet that this RuleSetManager can manage
	@Inject
	private T mRuleset;
	
	// Factory to get the appropriate Comparator based on ComparatorType 
	@Inject
	private ComparatorFactory mComparatorFactory;
	
	// Namespaces that Rules managed by this RuleSetManager know and use
	private Collection<Namespace> mRuleverse;

	// Store of processed RuleGroups for later use
	private Map<String, Pair<AbstractBaseRule, List<AbstractBaseRule>>> mMapOfRuleGroups;

	// Store of processed Rules for later use
	private Map<String, AbstractBaseRule> mMapOfRules;

	// String Literals
	private static final String RULE_XSD = "Rules.xsd";

	private static final String ELEMENT_NAME_NAMESPACE = "namespace";
	private static final String ELEMENT_NAME_PREFIX = "prefix";
	private static final String ELEMENT_NAME_URI = "uri";
	private static final String ELEMENT_NAME_RULEGROUP = "rulegroup";
	private static final String ELEMENT_NAME_RULE = "rule";
	private static final String ELEMENT_NAME_CONTROLLER = "controller";
	private static final String ELEMENT_NAME_NAME = "name";
	private static final String ELEMENT_NAME_TYPE = "type";
	private static final String ELEMENT_NAME_VALIDATES = "validates";
	private static final String ELEMENT_NAME_REFERENCES = "references";
	private static final String ELEMENT_NAME_REFERENCE = "reference";
	private static final String ELEMENT_NAME_SOURCE = "source";
	private static final String ELEMENT_NAME_ACCESSOR = "accessor";
	private static final String ELEMENT_NAME_ATTRIBUTE = "attribute";
	private static final String ELEMENT_NAME_EXCEPTION = "exception";
	private static final String ELEMENT_NAME_COMPARISON = "comparison";
	private static final String ELEMENT_NAME_OPERATOR = "operator";
	private static final String ELEMENT_NAME_METHOD = "method";
	private static final String ELEMENT_NAME_COMPARAND = "comparand";

	public RuleSetManager() throws RuleLoaderException {
		mRuleverse = new HashSet<Namespace>();
		mMapOfRuleGroups = new TreeMap<String, Pair<AbstractBaseRule, List<AbstractBaseRule>>>();
		mMapOfRules = new TreeMap<String, AbstractBaseRule>();
	}

	/**
	 * Load Rules by parsing and processing the Rules XML specific to this RuleSetManager
	 * Not to be invoked directly; is executed automatically as part of instantiation
	 */
	@PostConstruct
	private void loadRules() throws RuleLoaderException {
		log.debug("Processing rules ...");
		Element lRuleSet = null;
		
		/*
		 *  Parse the RuleSet XML validating against the Rule XSD
		 *  Retrieve the root element
		 *  Throw RuleLoaderException if unable to load the XML 
		 */
		try {
			lRuleSet = XmlUtils.parseXML(mRuleset.getRulesetXML(), RULE_XSD).getRootElement();
		} catch (JDOMException | IOException e) {
			throw new RuleLoaderException("Error loading rules.", e);
		}

		createRuleverseFromXML(lRuleSet);

		createRuleGroupsFromXML(lRuleSet);
		
		createRulesFromXML(lRuleSet);

		log.debug("Loaded " + mMapOfRuleGroups.size() + " rulegroup(s) and " + mMapOfRules.size() + " rule(s).");
	}

	/**
	 *  Parse all Namespace elements in this RuleSet XML
	 *  Create appropriate Namespace based on prefix and URI
	 *  Store for later use by Rules in XPath expressions
	 *  
	 *  @param aRuleSet	The root RuleSet element
	 */
	private void createRuleverseFromXML(Element aRuleSet) {
		ElementFilter lRuleverseFilter = new ElementFilter(ELEMENT_NAME_NAMESPACE, aRuleSet.getNamespace());
		Iterable<Element> lNamespaceElements = aRuleSet.getDescendants(lRuleverseFilter);

		for (Element lNSElement : lNamespaceElements) {
			Namespace lNamespace = Namespace.getNamespace(
					lNSElement.getChildText(ELEMENT_NAME_PREFIX, lNSElement.getNamespace()),
					lNSElement.getChildText(ELEMENT_NAME_URI, lNSElement.getNamespace()));
			mRuleverse.add(lNamespace);
		}
	}

	/**
	 *  Parse all RuleGroup elements in this RuleSet XML
	 *  Create appropriate RuleGroup with Rules based on RuleType and process/store other information
	 *  Store for later use
	 *
	 *  @param aRuleSet	The root RuleSet element
	 */
	private void createRuleGroupsFromXML(Element aRuleSet) {
		List<Element> lRuleGroups = aRuleSet.getChildren(ELEMENT_NAME_RULEGROUP, aRuleSet.getNamespace());
		
		for (Element lRuleGroupElement : lRuleGroups) {
			String lGroupName = lRuleGroupElement.getChildText(ELEMENT_NAME_NAME, lRuleGroupElement.getNamespace());
			log.debug("Processing RuleGroup " + lGroupName);
			
			// Identify, parse and load the Controller Rule
			Element lControllerElement = lRuleGroupElement.getChild(ELEMENT_NAME_CONTROLLER, lRuleGroupElement.getNamespace());
			AbstractBaseRule lController = parseRuleXMLElement(lControllerElement);
			lController.setupRule(mRuleverse);

			log.debug("Controling Rule is " + lController);
			
			// Parse and load the controlled Rules
			List<AbstractBaseRule> lRules = new Vector<AbstractBaseRule>();
			
			List<Element> lRuleElements = lRuleGroupElement.getChildren(ELEMENT_NAME_RULE, lRuleGroupElement.getNamespace());

			for (Element lRuleElement : lRuleElements) {
				AbstractBaseRule lRule = parseRuleXMLElement(lRuleElement);
				lRule.setupRule(mRuleverse);

				log.debug("Rule is " + lRule);
				
				lRules.add(lRule);
			}
			mMapOfRuleGroups.put(lGroupName, MutablePair.of(lController, lRules));
		}

	}

	/**
	 *  Parse all Rule elements in this RuleSet XML
	 *  Create appropriate Rule based on RuleType and process/store other information
	 *  Store for later use
	 *
	 *  @param aRuleSet	The root RuleSet element
	 */
	private void createRulesFromXML(Element aRuleSet) {
		List<Element> lRules = aRuleSet.getChildren(ELEMENT_NAME_RULE, aRuleSet.getNamespace());

		for (Element lRuleElement : lRules) {
			AbstractBaseRule lRule = parseRuleXMLElement(lRuleElement);
			lRule.setupRule(mRuleverse);

			log.debug("Rule is " + lRule);

			mMapOfRules.put(lRule.getName(), lRule);
		}
	}

	/**
	 *  Parse the Rule specified in this Rule XML element
	 *  Create appropriate Rule based on RuleType and process/store other information
	 *
	 *  @param aRuleElement	The Rule element
	 *  @return AbstractBaseRule The Rule as specified by the input XML element
	 */
	private AbstractBaseRule parseRuleXMLElement(Element aRuleElement) {
		log.debug("Requesting rule type " + aRuleElement.getChildText(ELEMENT_NAME_TYPE, aRuleElement.getNamespace()));

		// Create the appropriate rule based on the RuleType
		AbstractBaseRule lRule = mRuleFactory.getRule(RuleType.of(aRuleElement.getChildText(ELEMENT_NAME_TYPE,
				aRuleElement.getNamespace())));
		lRule.setName(aRuleElement.getChildText(ELEMENT_NAME_NAME, aRuleElement.getNamespace()));
		lRule.setValidates(aRuleElement.getChildText(ELEMENT_NAME_VALIDATES, aRuleElement.getNamespace()));
		lRule.setException(aRuleElement.getChildText(ELEMENT_NAME_EXCEPTION, aRuleElement.getNamespace()));

		// If <source> block exists, process <accessor> and all <attribute> children for dynamic rules.
		Element lRuleSource = aRuleElement.getChild(ELEMENT_NAME_SOURCE, aRuleElement.getNamespace());
		if (lRuleSource != null) {
			lRule.setAccessor(lRuleSource.getChildText(ELEMENT_NAME_ACCESSOR, aRuleElement.getNamespace()));
			List<String> lAttributesList = new Vector<String>();
			for (Element lAttribute : lRuleSource.getChildren(ELEMENT_NAME_ATTRIBUTE, lRuleSource.getNamespace())) {
				lAttributesList.add(lAttribute.getValue());
			}
			lRule.setAttributes(lAttributesList);
		}
		
		// If <references> block exists, process all <reference> children for linked rules.
		Element lReferenceList = aRuleElement.getChild(ELEMENT_NAME_REFERENCES, aRuleElement.getNamespace());
		if (lReferenceList != null) {
			for (Element lReference : lReferenceList.getChildren(ELEMENT_NAME_REFERENCE, lReferenceList.getNamespace())) {
				((AbstractLinkedRule) lRule).getReferences().add(lReference.getValue());
			}			
		}
		
		// If <comparison> block exists, process and set up the comparator.
		Element lComparisonElement = aRuleElement.getChild(ELEMENT_NAME_COMPARISON, aRuleElement.getNamespace());
		if (lComparisonElement != null) {
			ComparisonMethodType lMethodType = ComparisonMethodType.of(lComparisonElement.getChild(ELEMENT_NAME_METHOD, lComparisonElement.getNamespace()).getValue());
			AbstractComparator<?> lComparator = mComparatorFactory.getComparator(lMethodType);
			lComparator.setComparand(Integer.parseInt(lComparisonElement.getChild(ELEMENT_NAME_COMPARAND, lComparisonElement.getNamespace()).getValue()));
			lComparator.setOperatorType(OperatorType.of(lComparisonElement.getChild(ELEMENT_NAME_OPERATOR, lComparisonElement.getNamespace()).getValue()));
			lRule.setComparator(lComparator);			
		}
		
		return lRule;
	}

	/**
	 *  Apply Rules managed by this RuleSetManager on the input document
	 *
	 *  @param aInput	The Input document
	 *  @throws DynamicValidationException If validation fails
	 */
	public void applyRules(Document aInput) throws DynamicValidationException {
		log.debug("Applying rules ...");
		List<String> lAllMessages = new Vector<String>();
		
		// Process all RuleGroups
		for (Pair<AbstractBaseRule, List<AbstractBaseRule>> lRuleGroup : mMapOfRuleGroups.values()) {
			// Process the Controller Rule
			log.debug("Applying controller rule " + lRuleGroup.getLeft());
			Collection<String> lMessages = lRuleGroup.getLeft().applyRule(aInput);
			
			if (lMessages != null && lMessages.size() > 0) {
				// If Controller does not pass, pass along the message and do not process other rules in this RuleGroup
				lAllMessages.addAll(lMessages);				
			} else {
				// If Controller passes, process other rules
				for(AbstractBaseRule lRule : lRuleGroup.getRight()) {
					log.debug("Applying controlled rule " + lRule);
					lMessages = lRule.applyRule(aInput);
					if (lMessages != null) {
						lAllMessages.addAll(lMessages);
					}					
				}
			}
		}
		
		// Process all stand-alone Rules
		for (AbstractBaseRule lRule : mMapOfRules.values()) {
			log.debug("Applying rule " + lRule);
			Collection<String> lMessages = lRule.applyRule(aInput);
			if (lMessages != null) {
				lAllMessages.addAll(lMessages);
			}
		}
		
		// If any of the Rules failed to pass, throw an excepiton with the validation messages
		if (!lAllMessages.isEmpty()) {
			DynamicValidationException ex = new DynamicValidationException("Validation failed.");
			ex.setMessages(lAllMessages);
			throw ex;
		}
	}
}
